#include "TrackImplementation.h"

#include "FMODSystem.h"

#include "api/inc/fmod_errors.h"

#include <iostream>
#include <fstream>
#include <string>
#include <boost/config.hpp>
#include <boost/serialization/nvp.hpp>
#include "boost/archive/tmpdir.hpp"
#include "boost/archive/xml_iarchive.hpp"
#include "boost/archive/xml_oarchive.hpp"
#include "boost/archive/text_iarchive.hpp"
#include "boost/archive/text_oarchive.hpp"

namespace SnackBreak
{
namespace Impl
{

/*static*/ const double FMODTrack::CuePauseFrameLength = 0.1; // this keeps getting bigger
/*static*/ const double FMODTrack::CuePauseNudgeProportion = 0.25; // of CuePauseFrameLength, i.e. nudge is by partial frames

FMODTrack::FMODTrack(std::string f, CuePair q)
: 
	Track(f, q),
	fmodSound(NULL),
	fmodChannel(NULL),
	defaultSampleRate(44100.0)
{
	Init();
}

FMODTrack::FMODTrack(std::string f, CuePair q, double lastBeatNum)
: 
	Track(f, q, lastBeatNum),
	fmodSound(NULL),
	fmodChannel(NULL),
	defaultSampleRate(44100.0)
{
	Init();
}

void FMODTrack::Init()
{
	FMOD_RESULT result;

	// init the track with FMOD_SOFTWARE so we can use sample accurate sync callbacks
	result = System::Get().createSound(filePathName.c_str(), (FMOD_MODE)(FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM), 0, &fmodSound);
	ERRCHECK(result);

	// wahey, start the track now, we have no need to wait !
	result = System::Get().playSound(FMOD_CHANNEL_FREE, fmodSound, true, &fmodChannel);
	ERRCHECK(result);

	// determine the (normal, 100%) sample rate
	float tmp;
	result = fmodChannel->getFrequency(&tmp);
	defaultSampleRate = tmp;
	ERRCHECK(result);

	// default to 70% level so we have some headroom
	setVolume(0.7);
}

FMODTrack::~FMODTrack()
{
	FMOD_RESULT result;
	result = fmodSound->release();
	ERRCHECK(result);
}

// Set the current play position in beats.
void FMODTrack::setPosition(double beatNumber) const
{
	FMOD_RESULT result;
	result = fmodChannel->setPosition(cueInfo.myNthBeatIsAtMsec(beatNumber), FMOD_TIMEUNIT_MS);
	ERRCHECK(result);
}

// Get the current play position in beats.
double FMODTrack::getPosition() const
{
	FMOD_RESULT result;
	unsigned int msec = 0;
	result = fmodChannel->getPosition(&msec, FMOD_TIMEUNIT_MS);
	ERRCHECK(result);
	return cueInfo.msecToBeatNum(msec);
}

// Set the relative playback speed. Pass 1.0 for normal speed, 1.1 for +10% etc.
void FMODTrack::setPlayRate(double rateMultiplier) const
{
	FMOD_RESULT result;
	result = fmodChannel->setFrequency(defaultSampleRate * rateMultiplier);
	ERRCHECK(result);
}

// Set the volume/fader/level/amplitude. Pass 1.0 for max, 0 for silent.
void FMODTrack::setVolume(double vol) const
{
	FMOD_RESULT result;
	result = fmodChannel->setVolume(vol);
	ERRCHECK(result);
}

// Determine if track is playing (true) or paused (false).
bool FMODTrack::IsPlaying() const
{
	/*
	FMOD_RESULT result;
	bool paused = false;
	result = fmodChannel->getPaused(&paused);
	ERRCHECK(result);
	return !paused;*/
	return (currentMode == Track::Playing);
}

// Start or resume playback. (continues playing if already playing)
void FMODTrack::Play()
{
	if (currentMode == Track::CuePaused)
		UnSetupCuePause();

	FMOD_RESULT result;
	result = fmodChannel->setPaused(false);
	ERRCHECK(result);
	
	currentMode = Track::Playing;
}

// Pause playback.
void FMODTrack::Pause()
{
	if (currentMode == Track::CuePaused)
		UnSetupCuePause();

	FMOD_RESULT result;
	result = fmodChannel->setPaused(true);
	ERRCHECK(result);

	currentMode = Track::Paused;
}

// Enter Cue-Pause (CDJ-like frame loop for setting cue) mode.
void FMODTrack::CuePause()
{
	FMOD_RESULT result;
	result = fmodChannel->setPaused(true);
	ERRCHECK(result);
	
	SetupCuePause();

	result = fmodChannel->setPaused(false);
	ERRCHECK(result);

	currentMode = Track::CuePaused;
}

void FMODTrack::NudgeCuePause(int numFrames)
{
	if (currentMode == Track::CuePaused)
	{
	FMOD_RESULT result;
	result = fmodChannel->setPaused(true);
	ERRCHECK(result);
	
	double nudgeMsec = 1000.0 * numFrames * (CuePauseNudgeProportion * CuePauseFrameLength);
	SetupCuePause(cueLoop.cueLoopEnd + nudgeMsec);

	result = fmodChannel->setPaused(false);
	ERRCHECK(result);
    }
}

// Set simple CDJ-style single cue point, based on current play pos or cue-pause pos if in cue-pause mode.
// (This sets up "beats" to equal seconds in the normal cue scheme.)
void FMODTrack::SetSimpleCue()
{
	bool doit = false;
	double zeroBeatAtSec = 0;
	
	switch (getMode())
	{		
	// if playing use current pos for cue
	
	// if paused use current pos for cue
	
	// if cue-paused use end of cue-loop pos for cue
	case CuePaused:
		zeroBeatAtSec = cueLoop.cueLoopEnd / 1000.0;
		doit = true;
		break;

	// set cueinfo so beat zero is at cue time, and beat one is 1 sec after
	}
	
	if (doit)
	{
		TrackCuePair newInfo(CuePair(CuePoint(0, zeroBeatAtSec), CuePoint(1, zeroBeatAtSec+1.0)));
		setCueInfo(newInfo);
	}
}

// Saves cue info to an xml file alongside the file. This file if present will be read when the file is next opened.
void FMODTrack::AutoSaveCueInfo()
{

    std::string xpath(filePathName);
    xpath += ".snac.xml";
    std::ofstream ofs(xpath.c_str());
    assert(ofs.good());
    
    const TrackCuePair info(getCueInfo());
    boost::archive::text_oarchive oax(ofs);
    oax << (info);
//    boost::archive::xml_oarchive oax(ofs);
  //  oax << BOOST_SERIALIZATION_NVP(info);

}

// Loads cue info from an xml file alongside the file. 
void FMODTrack::AutoLoadCueInfo()
{
    std::string xpath(filePathName);
    xpath += ".snac.xml";
    std::ifstream ifs(xpath.c_str());
    assert(ifs.good());
    
    TrackCuePair info;
  //  boost::archive::xml_iarchive iax(ifs);
//    iax >> BOOST_SERIALIZATION_NVP(this);	    
    boost::archive::text_iarchive iax(ifs);
    iax >> (info);	    
    setCueInfo(info);

}


void FMODTrack::SetupCuePause(double desiredEndMsec)
{
	FMOD_RESULT result;

	unsigned int msec;
	if (desiredEndMsec < 0)
	{
		result = fmodChannel->getPosition(&msec, FMOD_TIMEUNIT_MS);
		desiredEndMsec = msec;
		ERRCHECK(result);
	}
	
	cueLoop.cueLoopEnd = desiredEndMsec;
	cueLoop.cueLoopStart = cueLoop.cueLoopEnd - (CuePauseFrameLength * 1000);
	if (cueLoop.cueLoopStart < 0)
	{
		// protect from loop prior to start of track!
		cueLoop.cueLoopEnd = (cueLoop.cueLoopStart - 0.0) + 0.000001;
		cueLoop.cueLoopStart = 0.0;
	}
	result = fmodChannel->setLoopPoints(cueLoop.cueLoopStart, FMOD_TIMEUNIT_MS, cueLoop.cueLoopEnd, FMOD_TIMEUNIT_MS);
	ERRCHECK(result);
	
	double tweakbacksoloopoccurs = 300; // is needed so the loop happens, sometimes it isn't noticed by the system. 300 msec seems to be the min to make it work on mac
	msec = cueLoop.cueLoopStart - tweakbacksoloopoccurs;
	msec = msec >= 0 ? msec : 0;
	result = fmodChannel->setPosition(msec, FMOD_TIMEUNIT_MS);
	ERRCHECK(result);
	
	result = fmodChannel->setMode(FMOD_LOOP_NORMAL);
	ERRCHECK(result);
}

void FMODTrack::UnSetupCuePause() const
{
	FMOD_RESULT result;
	result = fmodChannel->setMode(FMOD_LOOP_OFF);
	ERRCHECK(result);

// Should set position to end of cue pause frame?
//	unsigned int msec;
//	result = fmodChannel->getPosition(&msec, FMOD_TIMEUNIT_MS);
//	ERRCHECK(result);
	
	result = fmodChannel->setPosition(cueLoop.cueLoopEnd, FMOD_TIMEUNIT_MS);
	ERRCHECK(result);
}

}; // namespace Impl
}; // namespace SnackBreak

